Skip to content

fix(plugin): anchor WorktreeCreate at the project dir#3453

Merged
max-sixty merged 2 commits into
max-sixty:mainfrom
silvanshade:fix-worktree-create-hook
Jul 14, 2026
Merged

fix(plugin): anchor WorktreeCreate at the project dir#3453
max-sixty merged 2 commits into
max-sixty:mainfrom
silvanshade:fix-worktree-create-hook

Conversation

@silvanshade

@silvanshade silvanshade commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Anchor the WorktreeCreate hook at CLAUDE_PROJECT_DIR. The hook command inherits the session shell's working directory. When the shell has cd'd outside the repository (e.g. into a reference-papers directory elsewhere on disk), wt switch dies with not a git repository and every isolated-agent spawn in the session fails — observed as a six-agent spawn failure mid-session. cd "${CLAUDE_PROJECT_DIR:-.}" pins the hook to the repository the session was launched in, falling back to the current behavior when the variable is unset.

Originally this PR also added --base=@ to base agent worktrees on the session's HEAD. That change was dropped at the maintainer's request (worktrunk-bot pushed the removal) so it can be evaluated separately — hardcoding --base=@ would override the native worktree.baseRef setting for everyone, and the hook has no way to read that setting. This PR is now the cd anchor only.

🤖 Generated with Claude Code

…worktrees on HEAD

Two failure modes observed driving Claude Code agent isolation through the
plugin's WorktreeCreate hook:

1. The hook inherits the session shell's working directory. When the shell
   has cd'd outside the repository (e.g. into a reference-papers directory),
   wt switch dies with 'not a git repository' and every isolated-agent spawn
   in the session fails. Anchoring at CLAUDE_PROJECT_DIR (falling back to .
   when unset) pins the hook to the repo the session was launched in.

2. Without --base, agent worktrees are created from the default branch, so a
   sub-agent spawned mid-session on a feature branch is born at main and
   cannot see the session's committed work; its output then needs manual
   base correction before integration. --base=@ bases the worktree on the
   session's current HEAD, matching the isolated-copy-of-current-work
   expectation of sub-agent isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both changes check out. --base=@ is a documented shortcut (it appears in wt switch's own --long-help: wt switch --create fix --base=@ # Branch from current HEAD), and CLAUDE_PROJECT_DIR is a real Claude Code hook environment variable, so the cd "${CLAUDE_PROJECT_DIR:-.}" anchor is sound — and the :-. fallback preserves today's behavior when it's unset. Ordering is right too: the cd runs before wt switch, so @ resolves against the session's project repo rather than wherever the shell wandered. Creation-only, so no data-safety concern in the change itself.

One thing I can't approve on my own: this file (plugins/worktrunk/hooks/hooks.json) is on worktrunk's data-loss surface — it also ships the WorktreeRemove hook that runs wt remove — so per the repo's review policy any edit here goes to a human rather than a bot approval. Routing to @max-sixty for the merge call. The diff doesn't touch the removal path, so this is a policy hold, not a concern about the change.

Minor, non-blocking: this is a user-visible behavior change (agent worktrees now base on the session HEAD instead of the default branch). Worth a CHANGELOG entry, and maybe a line in skills/worktrunk/reference/claude-code.md where the WorktreeCreate/WorktreeRemove routing is described — though @max-sixty may prefer to author the changelog prose himself.

@worktrunk-bot
worktrunk-bot requested a review from max-sixty July 13, 2026 08:00
@max-sixty

Copy link
Copy Markdown
Owner

One thing I can't approve on my own: this file (plugins/worktrunk/hooks/hooks.json) is on worktrunk's data-loss surface — it also ships the WorktreeRemove hook that runs wt remove — so per the repo's review policy any edit here goes to a human rather than a bot approval. Routing to @max-sixty for the merge call. The diff doesn't touch the removal path, so this is a policy hold, not a concern about the change.

just as a process point, this is overly conservative, @worktrunk-bot you should be deciding based on whether the code actually touches code that is in data-destruction paths, not whether there are unrelated changes in the file; please fix the guidance here

@max-sixty

Copy link
Copy Markdown
Owner

thanks @silvanshade

Have you run with this at all yourself? I can test it myself for a while. I think your points all make sense; particularly (1); but I'm nervous about changes to this area of code given they're difficult to test.

@worktrunk-bot could you compare the default behavior of Claude Code to these?

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Compared both changes against Claude Code's native worktree behavior (docs hooks, and the claude-code CHANGELOG). Short version: (1) restores the native default; (2) diverges from it — which is the one worth a second look.

1. cd "${CLAUDE_PROJECT_DIR:-.}" — matches native. WorktreeCreate fires to "replace default git behavior" for --worktree / isolation: "worktree", and native creation is anchored at the project root, not wherever the shell wandered. Our hook runs with the session's live cwd (the cwd field in the hook payload), so without the anchor it inherits a stray cd. CLAUDE_PROJECT_DIR is documented as "the project root" and is exported to every hook process, so this re-pins the hook to what native does. Low-risk, creation-only, and the :-. fallback keeps today's behavior when the var is unset.

2. --base=@ (base on HEAD) — diverges from the current default. This is the subtle one. Since claude-code v2.1.133, worktree.baseRef defaults to fresh — agent-isolation worktrees branch from origin/<default>, and head (branch from local HEAD) is opt-in. So:

  • Our current hook (no --basewt's default base = the default branch) roughly tracks the native fresh default.
  • --base=@ hardcodes the head behavior for everyone.

The catch the PR body already flags — the hook can't read worktree.baseRef — cuts both ways: whichever base we hardcode, we override the user's setting for the other. Today we silently pick fresh-ish; the PR silently picks head. Neither honors a user who set the opposite.

Two things that make me lean toward the change being reasonable despite that, but worth your call:

  • Anthropic themselves flip-flopped here: EnterWorktree was local-HEAD from 2.1.128, then 2.1.133 reverted the default back to fresh (origin/<default>) specifically because unpushed commits were being dropped — but kept head as a first-class supported mode. So head isn't exotic; it's just not the default.
  • For sub-agent isolation the "isolated copy of current work" expectation (the PR's argument for head) is genuinely stronger than for a fresh --worktree, which is more often "start clean off main."

One smaller pre-existing gap surfaced along the way: native fresh bases on origin/<default> (the pushed tip), whereas wt switch --create's default base is the local default branch. So even without this PR we don't exactly mirror fresh — we're one fetch behind it. Not something to fix here, just context for "how close is the current default, really."

Sources
  • Claude Code hooks docs — WorktreeCreate ("Replaces default git behavior"), CLAUDE_PROJECT_DIR ("the project root", exported to hook processes), the cwd payload field: https://code.claude.com/docs/en/hooks
  • claude-code CHANGELOG v2.1.133: "Added worktree.baseRef setting (fresh | head) … the default fresh changes EnterWorktree's base back to origin/<default> (it has been local HEAD since 2.1.128) — set worktree.baseRef: "head" to keep unpushed commits in new worktrees."
  • claude-code CHANGELOG v2.1.128 (approx): EnterWorktree creates the branch from local HEAD instead of origin/<default>.
  • wt switch --create default base = default branch; --base=@ = "Branch from current HEAD" (wt switch --long-help).

On the "have you run it" question — that's for @silvanshade; I can't drive a live Claude Code session from CI to observe the hook end-to-end, so I've verified the semantics from source/docs rather than by running it.

@silvanshade

Copy link
Copy Markdown
Contributor Author

Have you run with this at all yourself? I can test it myself for a while. I think your points all make sense; particularly (1); but I'm nervous about changes to this area of code given they're difficult to test.

Yes, I have been running with this configuration since Fable identified it as a fix to workflow problems we were having.

The scenario described is real, I have a few separate repos which are topically related but not formally connected via submodules or anything like that, and then additional directories containing relevant research artifacts.

Prior to the change, worker subagents would sometimes get tripped up during tasks that would involve navigating around to do some analysis and then interacting with worktrunk.

Since the change, reports about those issues from the coordinator agents have stopped. In that regard it seems to have addressed the problem. Whether it's actually the right solution here, I don't know.

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Agreed — the hold should key on whether the diff actually touches a data-destruction path, not on unrelated contents of the same file. Fixed the guidance in #3457.

That PR rewrites the data-loss review rule so the file-level trigger only fires when the change can reach a destructive path: source-code edits still hold on proximity (a change near the force-delete path in src/git/remove.rs can alter its behavior without the destructive line appearing in the diff), but a structured config of independent entries like plugins/worktrunk/hooks/hooks.json only holds when the diff touches the destructive entry itself. Editing the WorktreeCreate command can't change what the separate WorktreeRemove hook runs — so this PR wouldn't have been held under the corrected rule.

This PR (#3453) is still yours to merge on the substance — I flagged the --base=@ divergence for your call and you wanted to run it yourself first; that stands independently of the process fix.

@max-sixty

Copy link
Copy Markdown
Owner

but kept head as a first-class supported mode

how is this mode selected?

@max-sixty

Copy link
Copy Markdown
Owner

happy to merge the cd now!

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

It's a settings key, not a flag or a per-run choice — worktree.baseRef in settings.json, accepting only "fresh" (default) or "head":

{
  "worktree": {
    "baseRef": "head"
  }
}

Default (fresh) branches new worktrees from origin/HEAD, falling back to local HEAD only if there's no remote or the fetch fails. Setting it to "head" always branches from local HEAD, carrying unpushed commits and feature-branch state — which the docs call out as "useful when isolating subagents that need to operate on in-progress work," i.e. exactly @silvanshade's sub-agent case.

Two things worth noting for this PR:

  • Subagent worktrees honor the same setting — the docs state subagent worktrees "use the same base branch as --worktree, so they branch from your repository's default branch unless worktree.baseRef is set to "head"." So a user can already get head behavior for agent isolation via config, without this PR.
  • That's the crux of the gap I flagged. Because it's a settings.json key and not something the WorktreeCreate hook can read from its stdin payload, our hook can't branch on it — so hardcoding --base=@ picks head for everyone and overrides a user who left the default fresh (just as today's no---base hook overrides a user who set head). Neither honors the setting; the PR just flips which default we silently impose.

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

One mechanical note on "merge the cd now": both changes are on the same line of hooks.json, so merging #3453 as-is pulls in --base=@ along with the cd anchor — there's no way to land just the cd by merging this PR.

To get the cd in now while keeping --base=@ here for your own testing, it needs a split. @silvanshade offered to split above; if it's quicker, I can open a cd-only PR (just the cd "${CLAUDE_PROJECT_DIR:-.}" anchor, no --base=@) for you to merge immediately, leaving this one open for the --base=@ call. Happy to do that on a word.

@max-sixty

Copy link
Copy Markdown
Owner

@worktrunk-bot can you remove the --base here so that @silvanshade keeps the PR?

@max-sixty

Copy link
Copy Markdown
Owner

It's a settings key, not a flag or a per-run choice — worktree.baseRef in settings.json, accepting only "fresh" (default) or "head":

Do plugins have access to this config?

Keep the CLAUDE_PROJECT_DIR anchor but remove --base=@ so the hook no
longer hardcodes head-based worktrees. Base selection stays as wt's
default (the default branch), matching Claude Code's native 'fresh'
default, rather than overriding the worktree.baseRef setting for
everyone.
@worktrunk-bot worktrunk-bot changed the title fix(plugin): anchor WorktreeCreate at the project dir and base agent worktrees on HEAD fix(plugin): anchor WorktreeCreate at the project dir Jul 14, 2026
@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Done — pushed a20d1c0 dropping --base=@ while keeping the cd "${CLAUDE_PROJECT_DIR:-.}" anchor, so this stays @silvanshade's PR and is now the cd-only change. Updated the title/description to match; the --base=@ question is out of scope here and can be taken up separately.

Do plugins have access to this config? No — not through any sanctioned channel. The WorktreeCreate hook can't see worktree.baseRef:

  • It's not in the hook's stdin payload. Per the hooks reference, WorktreeCreate receives session_id, transcript_path, cwd, hook_event_name, name, and isolation — no base/baseRef field.
  • It's not an exported env var. Hooks get CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, etc. — paths, not settings values.
  • And when a WorktreeCreate hook is configured, it "replaces the default git worktree logic entirely" — so Claude Code never runs its own base-selection in that path, and worktree.baseRef isn't consulted at all. The hook owns creation.

The only way the hook could reach the value is to read .claude/settings.json off disk itself and re-implement the full settings-precedence merge (enterprise → user → project → local → CLI). That's fragile and unsupported — which is exactly why hardcoding either base in the hook silently overrides whichever baseRef the user set. That's the gap behind the --base=@ call, and it's why it belongs in its own discussion rather than riding in with the cd fix.

Sources
  • WorktreeCreate input schema (fields: session_id, transcript_path, cwd, hook_event_name, name, isolation): hooks reference → WorktreeCreate
  • "For full control over how worktrees are created, configure a WorktreeCreate hook, which replaces the default git worktree logic entirely" and the worktree.baseRef "fresh"/"head" values: worktrees → Choose the base branch
  • Hook environment variables (CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, …): hooks reference

@max-sixty
max-sixty merged commit c4673bf into max-sixty:main Jul 14, 2026
39 checks passed
@max-sixty

Copy link
Copy Markdown
Owner

OK, @silvanshade — my sense is we should stay with the default, at least until CC allows for reading its config — how do you feel about that?

We could also quite easily add a config for default args for the hook — lmk and I'll implement?

judewang added a commit to judewang/worktrunk that referenced this pull request Jul 16, 2026
The WorktreeRemove hook inherits the working directory it is fired from.
When that is not inside the repo (e.g. the session's worktree was already
removed, or the hook fires from a neutral cwd), `wt remove <path>` dies with
`fatal: not a git repository` — repo discovery starts from cwd, not the path
argument. Claude Code surfaces this as `WorktreeRemove hook failed` and keeps
the session row, so the background session becomes undeletable.

`cd "${CLAUDE_PROJECT_DIR:-.}"` pins the hook to the launch repo, mirroring
the WorktreeCreate anchor added in max-sixty#3453.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants